﻿using System;
using System.Windows;
using System.Linq;
using PlottingBasics.LINQHelpers;
using PlottingBasics.Functions;
using System.Collections.Generic;

namespace PlottingBasics.PlotManipulations
{
    /// <summary>
    /// Helper clas that will calculate the integral of a curve, etc.
    /// </summary>
    public static class IntegralCalculations
    {
        /// <summary>
        /// returns the integral version of whatever points we hand in.
        /// </summary>
        /// <param name="pointsArray"></param>
        /// <returns></returns>
        public static IEnumerable<Point> IntegralCurve(this IEnumerable<Point> pointsArray)
        {
            var deltas = from d in pointsArray.Delta(p => p.X)
                         select d;
            var areas = from cb in pointsArray.TakeAllBut(1).Zipper(deltas)
                        select cb._first.Y * cb._second;
            var runningSum = (from a in areas.RunningSum((nv, ov) => nv + ov, 0.0)
                              select a._second).ToArray();

            ///
            /// Deal with special cases
            /// 

            if (runningSum.Length == 0)
            {
                if (pointsArray.Count() == 0)
                {
                    return new Point[0];
                }
                else
                {
                    return new Point[] { new Point(pointsArray.First().X, 1.0) };
                }
            }

            double maxValue = runningSum.Last();

            ///
            /// We are ready now to return an integral curve. The first value is always zero, obviously,
            /// and then the later values are the addition of above, divided, of course, by the max value.
            /// 

            var integral = from a in new double[] {0.0}.Concat(runningSum).Zipper(pointsArray)
                           select new Point(a._second.X, a._first/maxValue);

            return integral;
        }

        /// <summary>
        /// Flatten a sequence of points for those that fractionally probabiility is less than the cut.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="fractionCut"></param>
        /// <returns></returns>
        public static IEnumerable<Point> FlattenCurveLessThanIntegralCut(this IEnumerable<Point> input, double fractionCut)
        {
            double x;
            return FlattenCurveLessThanIntegralCut(input, fractionCut, out x);
        }

        /// <summary>
        /// Given a sequence of points, a probability curve, flatten them marked before they add to one.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="fractionCut"></param>
        /// <returns></returns>
        public static IEnumerable<Point> FlattenCurveLessThanIntegralCut(this IEnumerable<Point> input, double fractionCut, out double xTurnOn)
        {
            Pair<double, double> betweenGuys;
            double fraction;
            xTurnOn = XPosOfRemainingProbability(input, fractionCut, out betweenGuys, out fraction);

            ///
            /// Special cases, which happen sometimes! Yuck!
            ///  - If there is no cut before the first one, then we return the whole thing
            ///  - if it is after the last one (weird!) it is all flat!
            /// 

            if (xTurnOn == input.First().X)
            {
                return input;
            }

            if (xTurnOn == input.Last().X)
            {
                return new Point[] {
                    new Point (input.First().X, 0.0),
                    new Point (input.Last().X, 0.0)
                };
            }

            ///
            /// Ok - we now need to put together a new sequence. Find the fractional distance between where the cut happens.
            /// 

            double y1 = (from p in input where p.X == betweenGuys._first select p.Y).First();
            double y2 = (from p in input where p.X == betweenGuys._second select p.Y).First();

            double yinterp = Helpers.CalcFractionBetween(y1, y2, fraction);

            ///
            /// Ok, now we need to put together a new sequence. Things are a bit funny since we hvae to deal with
            /// three sequences here, before the transition, the transition itself, and after the transition.
            /// 

            Point[] firstPart = new Point[] {
                new Point(input.First().X, 0.0),
                new Point(xTurnOn*0.999, 0.0),
                new Point(xTurnOn, yinterp)
            };

            double turnOn = xTurnOn;
            var pointsPastX = from p in input
                              where p.X >= turnOn
                              select p;

            return firstPart.Concat(pointsPastX);
        }

        /// <summary>
        /// Returns the fraction of probability in the curve after the xpos position.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="xpos"></param>
        /// <returns></returns>
        public static double FractionProbabilityBeforeX(this IEnumerable<Point> input, double xpos)
        {
            var integralCurve = input.IntegralCurve();
            var adjacent = integralCurve.AdjacentItems(xpos, pt => pt.X);

            ///
            /// Special cases
            /// 

            if (adjacent._first == default(Point) && adjacent._second == default(Point))
            {
                /// Before the start or null sequence!
                if (input.Any())
                {
                    return 0.0;
                }
                else
                {
                    throw new ArgumentException("Can't find probability before x for a null sequence");
                }
            }

            if (adjacent._second == default(Point))
            {
                /// After the end!
                return 1.0;
            }

            ///
            /// Ok - now we will have to interpolate!
            ///

            double fractionBetween = Helpers.FractionBetween(adjacent._first.X, adjacent._second.X, xpos);
            double yfrac = Helpers.CalcFractionBetween(adjacent._first.Y, adjacent._second.Y, fractionBetween);

            return yfrac;
        }

        /// <summary>
        /// Returns the x-position of the probability left.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="probRight"></param>
        /// <returns></returns>
        public static double XPosOfRemainingProbability(this IEnumerable<Point> input, double probRight)
        {
            Pair<double, double> between;
            double fraction;
            return XPosOfRemainingProbability(input, probRight, out between, out fraction);
        }

        /// <summary>
        /// Do the calc, but return some extra info that makes this useful
        /// </summary>
        /// <param name="input"></param>
        /// <param name="probRight"></param>
        /// <returns></returns>
        private static double XPosOfRemainingProbability(this IEnumerable<Point> input, double probRight, out Pair<double, double> betweenGuysXPos, out double fraction)
        {
            if (probRight > 1.0
            || probRight < 0.0)
            {
                throw new ArgumentException("Probability to right can't be more than 1.0 or less than zero!");
            }

            ///
            /// Find where we should be making the cut
            /// 

            var integralCurve = input.IntegralCurve();
            var betweenGuys = integralCurve.AdjacentItems(probRight, a => a.Y);
            betweenGuysXPos = new Pair<double, double>(betweenGuys._first.X, betweenGuys._second.X);
            fraction = 0.0;

            ///
            /// Special cases, which happen sometimes! Yuck!
            ///  - If there is no cut before the first one, then we return the whole thing
            ///  - if it is after the last one (weird!) it is all flat!
            /// 

            if (betweenGuys._first == default(Point)
                && betweenGuys._second == default(Point)
                && !input.Any())
            {
                throw new ArgumentException("Empty sequence - can't figure out how to calc remaining probability");
            }

            if (betweenGuys._first == default(Point)
                && betweenGuys._second == integralCurve.First())
            {
                // We are before the first left edge
                return input.First().X;
            }

            if (betweenGuys._second == default(Point)
                && betweenGuys._first == integralCurve.Last())
            {
                // We are after the left edge
                return input.Last().X;
            }

            ///
            /// Now, find the actual x value where this happens.
            /// 

            fraction = Helpers.FractionBetween(betweenGuys._first.Y, betweenGuys._second.Y, probRight);
            double xTurnOn = Helpers.CalcFractionBetween(betweenGuys._first.X, betweenGuys._second.X, fraction);

            return xTurnOn;
        }

    }
}
